﻿//
// ArtistPhotosControl.cs: Control for managing artist photos within
// the artist library.
//
// Copyright 2010 Michael Goethe
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using S9PalCommon;
using S9PalCommon.ArtistLibrary;

namespace LastFMPhotoCrawler
{
    /// <summary>
    /// Control for managing artist photos within the artist library.
    /// </summary>
    public partial class ArtistPhotosControl : UserControl, IMainControl
    {
        #region Fields

        // Imports
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool GetCursorPos(out Point lpPoint);

        // Delegates
        private delegate void RefreshArtistPhotosControlDelegate();

        // Constants
        private const String c_CheckButtonFileName = "check.png";
        private const String c_CheckHoverButtonFileName = "checkhover.png";
        private const String c_XButtonFileName = "x.png";
        private const String c_XHoverButtonFileName = "x.png";

        // Variables
        private readonly Color c_artistPhotoPictureBoxBackColor = Color.LightGray;
        private readonly Color c_indexNumberFillColor = Color.FromArgb(128, 255, 255, 255);
        private readonly Color c_indexNumberStrokeColor = Color.FromArgb(100, 0, 0, 0);
        private readonly RectangleF c_syncButtonFloatRect = new RectangleF(0.7f, 0.05f, 0.05f, 0.05f);

        private bool controlCanUpdate = true;
        private int columnMax;                                      // Maximum number of columns that can be displayed without using a horizontal bar. Calculated with the width of the control.
        private Artist currentArtist = null;                        // Class copy of the currently selected artist.
        private bool isHoveringOverSyncButton;                      // True if the mouse is hovering over the sync button (check/x).
        private Dictionary<String, Bitmap> loadedPhotos;            // Dictionary of photos loaded from files.
        private bool mouseDrag = false;                             // True if the mouse is down on a picture box, meaning the pic box will now follow mouse movements. 
        private Point mouseDragOffset = new Point(0, 0);            // When a MouseDown occurs, this holds the position of the mouse relative to the top-left of the picture box.
        private int numberOfSyncPhotos;                             // Number of the artist photos that are set to sync.
        private int originalIndex;                                  // Control index of a picture box at the start of a drag.
        private Dictionary<String, Bitmap> resizedPhotos;           // Dictionary of photos resized from the loaded photos.
        private Rectangle syncButtonPixelRect;                      // Coordinates for rendering the sync button. Calculated with the above FloatRect and the picture box size.
        private FileSystemWatcher watcher = new FileSystemWatcher();// Watches for activity in the Library directory.
        
        // Property variables
        private int childPadding = 10;
        private String displayTitle = "Artist Photos";
        private int maxWidthHeight = 250;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates an ArtistPhotosControl object.
        /// </summary>
        public ArtistPhotosControl()
        {
            InitializeComponent();

            // Add an event handler to catch when the selected artist has been changed.
            S9PalCommon.Global.SelectedArtistChanged += new EventHandler(Global_SelectedArtistChanged);

            // Update the maxWidthHeight from the Settings and adjust the corresponding track bar.
            if (Global.Settings.ArtistPhotoDisplaySizeFactor > 0)
            {
                maxWidthHeight = Global.Settings.ArtistPhotoDisplaySizeFactor * 50;
                photoSizeTrackBar.Value = Global.Settings.ArtistPhotoDisplaySizeFactor;
            }

        }

        /// <summary>
        /// Creates an ArtistPhotosControl object and receives events from a watcher.
        /// </summary>
        /// <param name="watcher">Watcher to handle events from.</param>
        public ArtistPhotosControl(ArtistPhotoWatcher watcher) : this()
        {
            watcher.ArtistPhotoAdded += new ArtistEventHandler(watcher_ArtistPhotoAdded);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the display title of the component.
        /// </summary>
        public String DisplayTitle
        {
            get
            {
                return displayTitle;
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Change the artist photo to the specified sync status.
        /// </summary>
        /// <param name="photo">ArtistPhoto to change.</param>
        /// <param name="sync">Boolean value specifying whether to sync or not.</param>
        private void ChangeArtistPhotoSyncStatus(ArtistPhoto photo, bool sync)
        {
            // If the value hasn't changed, we want to make sure not to do anything.
            if (photo.SyncPhoto != sync)
            {
                if (sync)
                {
                    // Move the photo to the end of the synced photos.
                    MoveArtistPhoto(photo, numberOfSyncPhotos);
                    // Set the photo's SyncPhoto property to true.
                    photo.SyncPhoto = true;
                    // Increment the number of synced photos so we can continue to add things to the end of them.
                    numberOfSyncPhotos++;
                }
                else
                {
                    // Move the artist photo to the end of ALL of the photos.
                    MoveArtistPhoto(photo, currentArtist.Photos.Count - 1);
                    // Set the photo's SyncPhoto property to false.
                    photo.SyncPhoto = false;
                    // Decrement the number of synced photos.
                    numberOfSyncPhotos--;
                }
            }
        }

        /// <summary>
        /// Adds a number of picture boxes and their event handlers to the artistsPhotosPanel.
        /// </summary>
        /// <param name="numberOfPictureBoxes">Number of picture boxes to add to artistsPhotosPanel.</param>
        private void CreatePhotoPictureBoxes(int numberOfPictureBoxes)
        {
            // Clear the old controls first.
            this.artistPhotosPanel.Controls.Clear();

            for (int i = 0; i < numberOfPictureBoxes; i++)
            {
                PictureBox artistPhotoPictureBox = new PictureBox();

                // Set the BackColor of each picture box.
                artistPhotoPictureBox.BackColor = c_artistPhotoPictureBoxBackColor;

                // Add a plethora of event handlers for each picture box.
                artistPhotoPictureBox.MouseDown += new MouseEventHandler(artistPhotoPictureBox_MouseDown);
                artistPhotoPictureBox.MouseMove += new MouseEventHandler(artistPhotoPictureBox_MouseMove);
                artistPhotoPictureBox.MouseUp += new MouseEventHandler(artistPhotoPictureBox_MouseUp);
                artistPhotoPictureBox.MouseClick += new MouseEventHandler(artistPhotoPictureBox_MouseClick);
                artistPhotoPictureBox.MouseLeave += new EventHandler(artistPhotoPictureBox_MouseLeave);

                // Add the picture box to the artistPhotosPanel.
                this.artistPhotosPanel.Controls.Add(artistPhotoPictureBox);
            }
        }

        /// <summary>
        /// Draws a "heads-up display," which consists of an index number and sync button, onto a bitmap.
        /// </summary>
        /// <param name="bitmap">Original bitmap to draw on. (This object is not affected. Return contains the new bitmap.)</param>
        /// <param name="indexNumber">Index number to render to the bitmap.</param>
        /// <param name="syncButtonFileName">Sync button file name (as contained in Resources) to render to the bitmap.</param>
        /// <returns>Bitmap with heads-up display information.</returns>
        private Bitmap DrawHUDOnBitmap(Bitmap bitmap, int indexNumber, String syncButtonFileName)
        {
            Graphics gfx;                                   // Drawing surface that we'll use to draw our index number and sync button on.
            GraphicsPath gfxPath = new GraphicsPath();      // This graphics path will be used for creating a path around some text. It can then be filled and stroked with different color for readability.
            Bitmap bitmapCopy = (Bitmap)bitmap.Clone();     // We don't want to alter our original bitmap, so this will make a copy of it.

            // Set surface to copy of original bitmap.
            gfx = Graphics.FromImage(bitmapCopy);
            // Set the following properties to force anti-aliasing and high quality smoothing (so our numbers aren't as jagged).
            gfx.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
            gfx.SmoothingMode = SmoothingMode.HighQuality;

            // Render the index number with a fill and stroke.
            gfxPath.AddString(indexNumber.ToString(), new FontFamily("Arial"), (int)FontStyle.Bold, maxWidthHeight / 20 + 30, new Point(0, 0), StringFormat.GenericDefault);
            gfx.FillPath(new SolidBrush(c_indexNumberFillColor), gfxPath);
            gfx.DrawPath(new Pen(new SolidBrush(c_indexNumberStrokeColor), 1), gfxPath);

            // Use the float rect (defined in the class fields) to set the pixel rect. This approach is useful because the user can adjust the bitmap size, and we want our button 
            // to grow and shrink a bit depending on the bitmap's current size.
            syncButtonPixelRect = new Rectangle(0, 0, (int)(maxWidthHeight * c_syncButtonFloatRect.Width) + 20, (int)(maxWidthHeight * c_syncButtonFloatRect.Height) + 20);
            syncButtonPixelRect.X = maxWidthHeight - syncButtonPixelRect.Width - 10;
            syncButtonPixelRect.Y = 10;

            // Render the sync button.
            gfx.DrawImage(new Bitmap(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("LastFMPhotoCrawler.Resources." + syncButtonFileName)), syncButtonPixelRect);

            return bitmapCopy;
        }

        /// <summary>
        /// Fills the photos picture boxes with a Dictionary of Bitmaps.
        /// </summary>
        /// <param name="bitmaps">Bitmaps to use to fill.</param>
        private void FillPhotoPictureBoxes(Dictionary<String, Bitmap> bitmaps)
        {
            List<ArtistPhoto> photos = S9PalCommon.Global.ArtistLibrary.Artists[currentArtist.Name].Photos;
            
            for (int i = 0; i < this.artistPhotosPanel.Controls.Count; i++)
                (this.artistPhotosPanel.Controls[i] as PictureBox).Image = bitmaps[photos[i].LocalFileName];
        }

        /// <summary>
        /// Load artist photos from the paths specified in the library.
        /// </summary>
        /// <returns>Dictionary of bitmaps containing the loaded photos.</returns>
        private Dictionary<String, Bitmap> LoadArtistPhotosFromFiles()
        {
            List<ArtistPhoto> photos = S9PalCommon.Global.ArtistLibrary.Artists[currentArtist.Name].Photos;     // All the photos for the current artist.
            List<ArtistPhoto> photosToRemove = new List<ArtistPhoto>();                                         // List that we'll add to if a photo needs to be removed.
            Dictionary<String, Bitmap> loadedPhotos = new Dictionary<String, Bitmap>();                         // All the loaded photos, which once filled, will be returned at the end of the method.

            // Loop through the artist photos and load images into loadedPhotos.
            for (int i = 0; i < photos.Count; i++)
            {
                String photoFileName = photos[i].LocalFileName;
                String artistDirectory = Helper.RemoveInvalidFileNameCharacters(currentArtist.Name);
                String path = S9PalCommon.Global.ArtistLibrary.LibraryDirectory + "\\" + artistDirectory + "\\" + photoFileName;    // Path to the photo.
                Bitmap bitmap = null;
                FileStream fs = null;

                try
                {
                    // Attempt to create a file stream from the file.
                    fs = new FileStream(path, FileMode.Open, FileAccess.Read);
                    // Construct a bitmap from the stream.
                    bitmap = new Bitmap(Image.FromStream(fs));
                }
                catch (FileLoadException)
                {
                    // This can get thrown when if the file is still locked elsewhere (ie. by the crawler downloading it). This most often
                    // occurs on slower internet connections. Another exception that should correct itself over time, so continue.
                    continue;
                }
                catch (ArgumentException)
                {
                    // This will get thrown when the image fails to load from the stream, which generally occurs when a file is incomplete.
                    // For instance, if the user clicks on an artist for whom photos are currently being downloaded, this could possibly get thrown.
                    // It's something that should correct itself with time, so simply continue for now.
                    continue;
                }
                catch (DirectoryNotFoundException)
                {
                    // This will get thrown if the directory couldn't be found. Obviously this means the photo isn't there either, so add it to a list
                    // of photos to remove, so we can get rid of it further down.
                    photosToRemove.Add(photos[i]);
                    continue;
                }
                catch (FileNotFoundException)
                {
                    // This will get thrown if the image couldn't be found. Add the photo to a list of photos to remove, so we can get rid of it further down.
                    // I considered adding a dialog box to see if the user in fact wanted to remove these files, but I decided against it because I didn't
                    // think it would have much purpose. I mean, why would a user want to keep a reference to a file that no longer existed?
                    photosToRemove.Add(photos[i]);
                    continue;
                }
                catch (IOException)
                {
                    // This could get thrown under conditions similar to those outlined for ArgumentException.
                    // Again, it should correct itself with time, so just continue for now.
                    continue;
                }
                finally
                {
                    // Close the stream if need be.
                    if (fs != null)
                        fs.Close();
                }

                // Add the bitmap to loadedPhotos.
                loadedPhotos.Add(photoFileName, bitmap);
            }

            // Remove references to files that weren't found.
            for (int i = 0; i < photosToRemove.Count; i++)
                photos.Remove(photosToRemove[i]);

            return loadedPhotos;
        }

        /// <summary>
        /// Moves an ArtistPhoto to the specified index.
        /// </summary>
        /// <param name="artistPhotoToMove">ArtistPhoto object to move.</param>
        /// <param name="newIndex">New index that the ArtistPhoto object will be located at.</param>
        private void MoveArtistPhoto(ArtistPhoto artistPhotoToMove, int newIndex)
        {
            List<ArtistPhoto> artistPhotos = S9PalCommon.Global.ArtistLibrary.Artists[currentArtist.Name].Photos;
            int oldIndex = artistPhotos.IndexOf(artistPhotoToMove);
            int start;
            int end;

            // Remove the original photo.
            artistPhotos.Remove(artistPhotoToMove);

            // Because this method is getting called based upon user form interactions. There's the possiblity that
            // the newIndex could be well out of range (the user drags a photo way up into the upper left corner or
            // down past the set-to-sync photos). However, we know that the user's intention is to move this photo
            // to the beginning/end of the list, so we'll go ahead and account for those situations.
            if (newIndex < 0)
            {
                // Put it at the beginning. 
                newIndex = 0;
            }
            else if (newIndex > artistPhotos.Count)
            {
                // Put it at the end of the set-to-sync photos.
                newIndex = artistPhotos.Count;
            }

            // Insert at the index specified.
            artistPhotos.Insert(newIndex, artistPhotoToMove);

            // For photos affected by the move, flag them as needing to be re-synced.
            start = (newIndex < oldIndex) ? newIndex : oldIndex;
            end = (newIndex > oldIndex) ?  newIndex : oldIndex;
            for (int i = start; i <= end; i++)
                artistPhotos[i].NeedsToBeUpdated = true;
        }

        /// <summary>
        /// Draw all the picture boxes in the artistPhotosPanel.
        /// </summary>
        private void DrawArtistPhotosPanel()
        {
            List<ArtistPhoto> photos = S9PalCommon.Global.ArtistLibrary.Artists[currentArtist.Name].Photos;

            // Loop through, draw HUDs onto the photos, and set the picture boxes' Image property to the photos.
            for (int i = 0; i < this.artistPhotosPanel.Controls.Count; i++)
            {
                Bitmap bitmapWithHUD = null;

                try
                {
                    // First draw the applicable HUD on the bitmap. If the photo is set to sync, then draw a check.
                    // Otherwise, draw an x on the image.
                    if (photos[i].SyncPhoto)
                        bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[photos[i].LocalFileName], i + 1, "check.png");
                    else
                        bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[photos[i].LocalFileName], i + 1, "x.png");
                }
                catch (KeyNotFoundException)
                {
                    // Since we're looping through the ArtistPhoto objects, it's possible that, if photos are currently being added to the artist
                    // selected, we could end up with a photo in the library that hasn't yet been added to the resizedPhotos. We just want to
                    // continue as this will no longer be an issue once the photo has been entirely completed and is added to the resizedPhotos.
                    continue;
                }
                    
                // Set the Image property to the bitmap.
                (this.artistPhotosPanel.Controls[i] as PictureBox).Image = bitmapWithHUD;
                // Set the Tag property to the current ArtistPhoto object. This will allow us to tell which photo belongs to which picture box later on.
                this.artistPhotosPanel.Controls[i].Tag = photos[i];
            }

            // Arrange the picture boxes based on the size of the container.
            UpdateArtistPhotoPictureBoxLocations();
        }

        /// <summary>
        /// Refresh and redraw the entire ArtistPhotosControl.
        /// </summary>
        private void RefreshArtistPhotosControl()
        {
            // Reset and recalculate the number of sync photos. This variable is used for knowing where we need to
            // insert both newly set-to-sync and newly set-to-unsync photos.
            numberOfSyncPhotos = 0;
            for (int i = 0; i < currentArtist.Photos.Count; i++)
            {
                if (currentArtist.Photos[i].SyncPhoto)
                    numberOfSyncPhotos++;
            }

            // Load photos from files into the class-wide loadedPhotos.
            loadedPhotos = LoadArtistPhotosFromFiles();

            // Resize photos in loadedPhotos and set them to the class-wide resizedPhotos.
            resizedPhotos = ResizeMultipleBitmaps(loadedPhotos, maxWidthHeight);

            // Create all the picture boxes we'll need.
            CreatePhotoPictureBoxes(loadedPhotos.Count);

            // Redraw all the picture boxes and their contents.
            DrawArtistPhotosPanel();
        }

        /// <summary>
        /// Resize a bitmap based upon a maximum constraint.
        /// </summary>
        /// <param name="bitmapToResize">Bitmap that will be resized. (This object is not affected. Return contains the new bitmap.)</param>
        /// <param name="maxWidthHeight">The maximum width or height (depending on which is greater) that the resized image will have.</param>
        /// <returns>Newly resized bitmap.</returns>
        private Bitmap ResizeSingleBitmap(Bitmap bitmapToResize, int maxWidthHeight)
        {
            Bitmap resizedPhoto;                // Our soon-to-be resized photo. This will be returned at the end of the method.
            Graphics gfx;                       // Drawing surface that we'll use to resize our bitmap.
            int newWidth;                       // The exact resize width of the bitmap. (Not the maximum constraint.)
            int newHeight;                      // The exact resize height of the bitmap. (Again, not the maximum constraint.)

            // Set the greater of the width and height to the maxWidthHeight specified, and resize the other so that it follows
            // the original aspect ratio of the image.
            if (bitmapToResize.Width >= bitmapToResize.Height)
            {
                newWidth = maxWidthHeight;
                newHeight = (int)(bitmapToResize.Height / ((float)bitmapToResize.Width / maxWidthHeight));
            }
            else
            {
                newWidth = (int)(bitmapToResize.Width / ((float)bitmapToResize.Height / maxWidthHeight));
                newHeight = maxWidthHeight;
            }

            // Set the resizedPhoto to a new bitmap with our maxWidthHeight. We're not using the newWidth and newHeight here because
            // we still want the end product to be a square, even though the image will not likely fill it entirely.
            resizedPhoto = new Bitmap(maxWidthHeight, maxWidthHeight);
            // Set our graphics surface to the bitmap.
            gfx = Graphics.FromImage(resizedPhoto);
            // Adjust the InterpolationMode to ensure that we get the best resize quality.
            gfx.InterpolationMode = InterpolationMode.HighQualityBicubic;
            // Finally, take the original bitmap and draw it to the resizedPhoto graphics surface, according to the newWidth and newHeight.
            // We also want to center the image, which takes a little extra math for the x and y parameters.
            gfx.DrawImage(bitmapToResize, (maxWidthHeight - newWidth) / 2, (maxWidthHeight - newHeight) / 2, newWidth, newHeight);

            return resizedPhoto;
        }

        /// <summary>
        /// Resize a Dictionary of Bitmaps cataloged by Strings.
        /// </summary>
        /// <param name="bitmapsToResize">Dictionary of bitmaps to resize.</param>
        /// <param name="maxWidthHeight">New maximum width and height of the Bitmaps.</param>
        /// <returns></returns>
        private Dictionary<String, Bitmap> ResizeMultipleBitmaps(Dictionary<String, Bitmap> bitmapsToResize, int maxWidthHeight)
        {
            Dictionary<String, Bitmap> resizedBitmaps = new Dictionary<String, Bitmap>();

            // Loop through all the Bitmaps in the dictionary and use our ResizeSingleBitmap method to handle them one-by-one.
            foreach (KeyValuePair<String, Bitmap> kvp in bitmapsToResize)
                resizedBitmaps.Add(kvp.Key, ResizeSingleBitmap(kvp.Value, maxWidthHeight));

            return resizedBitmaps;
        }

        /// <summary>
        /// Updates the locations and sizes of the controls in the artistPhotosPanel.
        /// </summary>
        private void UpdateArtistPhotoPictureBoxLocations()
        {
            int column = 0;
            int row = 0;

            // Calculate the maximum number of columns based upon the current width of the artistPhotosPanel and the size of the picture box controls.
            columnMax = this.artistPhotosPanel.Width / (maxWidthHeight + childPadding);
            
            // Make sure we have at least one column to view, even if we have to use a horizontal scrollbar (yuck!).
            if (columnMax < 1) columnMax = 1;

            // Arrange our picture boxes in a grid format. Left to right first, then up and down.
            for (int i = 0; i < this.artistPhotosPanel.Controls.Count; i++)
            {
                // If we're going beyond the width of the control, then we want to wrap back around.
                if (column >= columnMax)
                {
                    column = 0;
                    row++;
                }
                
                // Calculate the location and size based upon the current maxWidthHeight and the childPadding.
                this.artistPhotosPanel.Controls[i].Location = new Point(column * (maxWidthHeight + childPadding) - this.artistPhotosPanel.HorizontalScroll.Value, row * (maxWidthHeight + childPadding) - this.artistPhotosPanel.VerticalScroll.Value);
                this.artistPhotosPanel.Controls[i].Size = new Size((int)(maxWidthHeight), (int)(maxWidthHeight));

                column++;
            }
        }

        #endregion

        #region Events

        private void ArtistPhotosControl_Resize(object sender, EventArgs e)
        {
            // When the control is resized, the positions of the picture boxes may need to be adjusted.
            UpdateArtistPhotoPictureBoxLocations();
        }

        private void artistPhotoPictureBox_MouseClick(object sender, MouseEventArgs e)
        {
            // If the sync button is clicked, change the sync status of the photo.
            if (e.Button == MouseButtons.Left && syncButtonPixelRect.Contains(e.Location))
            {
                ArtistPhoto photo = (ArtistPhoto)(sender as Control).Tag;
                ChangeArtistPhotoSyncStatus(photo, !photo.SyncPhoto);
                DrawArtistPhotosPanel();
            }
        }

        private void artistPhotoPictureBox_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left && !syncButtonPixelRect.Contains(e.Location) && ((sender as Control).Tag as ArtistPhoto).SyncPhoto)
            {
                // Disallow the ArtistPhotosControl from updating since we'll have a hold of one of its picture boxes.
                controlCanUpdate = false;

                // Set to drag mode and adjust the drag offset to compensate for the relative position of the mouse within the picture box.
                mouseDrag = true;
                mouseDragOffset = new Point(e.X, e.Y);

                // Set originalIndex to the child index that the picture box started at. We need this in MouseUp.
                // If we're moving the photo from a lower index to a higher index, we'll need to decrement the new index by 1.
                originalIndex = this.artistPhotosPanel.Controls.GetChildIndex((Control)sender);

                // Set the child index to 0 so that the dragging picture box hovers above the rest.
                this.artistPhotosPanel.Controls.SetChildIndex((Control)sender, 0);
            }
        }

        private void artistPhotoPictureBox_MouseLeave(object sender, EventArgs e)
        {
            // If the sync button is still on the hover version, we need to change it, since the mouse has left the control.
            if (isHoveringOverSyncButton)
            {
                int controlIndex = this.artistPhotosPanel.Controls.GetChildIndex((Control)sender);
                ArtistPhoto photo = (ArtistPhoto)(sender as Control).Tag;
                String localFileName = photo.LocalFileName;
                Bitmap bitmapWithHUD;

                // Reset bitmapWithHUD to the non-hover version.
                if (photo.SyncPhoto)
                    bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[localFileName], controlIndex + 1, c_CheckButtonFileName);
                else
                    bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[localFileName], controlIndex + 1, c_XButtonFileName);

                // Update the Image property.
                (this.artistPhotosPanel.Controls[controlIndex] as PictureBox).Image = bitmapWithHUD;

                isHoveringOverSyncButton = false;
            }

            controlCanUpdate = true;
        }

        private void artistPhotoPictureBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (mouseDrag)
            {
                // We're in drag mode, so make the picture box follow the cursor.
                Point p;
                GetCursorPos(out p);
                (sender as PictureBox).Left = p.X - mouseDragOffset.X - (sender as PictureBox).Parent.PointToScreen(new Point(0, 0)).X;
                (sender as PictureBox).Top = p.Y - mouseDragOffset.Y - (sender as PictureBox).Parent.PointToScreen(new Point(0, 0)).Y;
            }
            else
            {
                // If we're not dragging, then we want to check if the user is hovering over the sync button and update
                // the sync button to a hover version if so.
                int controlIndex = this.artistPhotosPanel.Controls.GetChildIndex((Control)sender);
                ArtistPhoto photo = (ArtistPhoto)(sender as Control).Tag;
                String localFileName = photo.LocalFileName;
                Bitmap bitmapWithHUD;

                if (syncButtonPixelRect.Contains(e.Location))
                {
                    // Draw the hover version of the HUD.
                    if (photo.SyncPhoto)
                        bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[localFileName], controlIndex + 1, c_CheckHoverButtonFileName);
                    else
                        bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[localFileName], controlIndex + 1, c_XHoverButtonFileName);

                    // Update the Image property.
                    (this.artistPhotosPanel.Controls[controlIndex] as PictureBox).Image = bitmapWithHUD;

                    isHoveringOverSyncButton = true;

                }
                else if (isHoveringOverSyncButton)
                {
                    // Draw the regular version of the HUD.
                    if (photo.SyncPhoto)
                        bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[localFileName], controlIndex + 1, c_CheckButtonFileName);
                    else
                        bitmapWithHUD = DrawHUDOnBitmap(resizedPhotos[localFileName], controlIndex + 1, c_XButtonFileName);

                    // Update the Image property.
                    (this.artistPhotosPanel.Controls[controlIndex] as PictureBox).Image = bitmapWithHUD;

                    isHoveringOverSyncButton = false;
                }
            }
        }

        private void artistPhotoPictureBox_MouseUp(object sender, MouseEventArgs e)
        {
            if (mouseDrag)
            {
                Point p;
                int newIndex;
                int column;
                int row;

                // Get the cursor's current screen position.
                GetCursorPos(out p);

                // Calculate the column and row based upon the cursor's current position.
                column = ((p.X - (sender as PictureBox).Parent.PointToScreen(new Point(0, 0)).X) + (maxWidthHeight + childPadding) / 2) / (maxWidthHeight + childPadding);
                row = ((this.artistPhotosPanel.VerticalScroll.Value + p.Y - (sender as PictureBox).Parent.PointToScreen(new Point(0, 0)).Y)) / (maxWidthHeight + childPadding);

                // Limit the column to the columnMax.
                if (column > columnMax)
                    column = columnMax;

                // Calculate the new index based upon the row and column.
                newIndex = row * columnMax + column;

                // If we're moving from a lower index to a higher index, we need to decrement the new index by 1.
                if (originalIndex < newIndex)
                    newIndex--;

                // Limit the newIndex to the numberOfSyncPhotos.
                if (newIndex > numberOfSyncPhotos - 1)
                    newIndex = numberOfSyncPhotos - 1;

                // Move the photo to the new index.
                MoveArtistPhoto((ArtistPhoto)(sender as Control).Tag, newIndex);

                // Redraw.
                DrawArtistPhotosPanel();

                mouseDrag = false;
            }

            controlCanUpdate = true;
        }

        private void browseForPhotosToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // The user wants to browse for a photo, so...

            // Create a new OpenFileDialog. Restrict the files to JPEG and allow multiselect.
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Filter = "JPEG Files (*.jpg, *.jpeg)|*.jpg;*.jpeg";
            ofd.Multiselect = true;
            // Show the open file dialog.
            ofd.ShowDialog(this);

            // Loop through all the files selected and import them.
            for (int i = 0; i < ofd.FileNames.Length; i++)
            {
                ArtistPhoto photo = new ArtistPhoto();
                String artistDirectory = Helper.RemoveInvalidFileNameCharacters(currentArtist.Name);
                bool retry = false;

                // Get the file name of the JPEG and set LocalFileName to it.
                photo.LocalFileName = Path.GetFileName(ofd.FileNames[i]);
                photo.UserAddedPhoto = true;

                do
                {
                    try
                    {
                        // Attempt to copy the file over to the artist's library directory.
                        File.Copy(ofd.FileNames[i], S9PalCommon.Global.ArtistLibrary.LibraryDirectory + "\\" + artistDirectory + "\\" + photo.LocalFileName);
                        retry = false;
                    }
                    catch (IOException ex)
                    {
                        // Check if this is a "file already exists" error.
                        if (ex.Message.EndsWith("already exists."))
                        {
                            // Prompt the user to see if they would like to replace the file or not.
                            DialogResult result = MessageBox.Show(ex.Message + " Would you like to replace this file?", "Error", MessageBoxButtons.YesNo, MessageBoxIcon.Error);

                            if (result == DialogResult.Yes)
                            {
                                // Delete the old file and retry the copy.
                                File.Delete(S9PalCommon.Global.ArtistLibrary.LibraryDirectory + "\\" + artistDirectory + "\\" + photo.LocalFileName);
                                retry = true;
                            }
                            else
                            {
                                return;
                            }
                        }
                        else
                        {
                            // We didn't handle the exception, so rethrow it.
                            throw ex;
                        }
                    }
                } while (retry);
            }
        }

        private void getArtistPhotosToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // Run a new LastFMPhotoCrawler for the artist selected.
            if (currentArtist != null)
            {
                LastFMPhotoCrawler lastFMPhotoCrawler = new LastFMPhotoCrawler(currentArtist);
                lastFMPhotoCrawler.Start();
            }
        }

        private void photoSizeTrackBar_Scroll(object sender, EventArgs e)
        {
            // Update the corresponding global, so we can keep this setting saved.
            Global.Settings.ArtistPhotoDisplaySizeFactor = photoSizeTrackBar.Value;

            // Recalculate the maxWidthHeight based upon the new value.
            maxWidthHeight = Global.Settings.ArtistPhotoDisplaySizeFactor * 50;

            // Re-instantiate the syncButtonPixelRect with the new maxWidthHeight.
            syncButtonPixelRect = new Rectangle((int)(maxWidthHeight * c_syncButtonFloatRect.X), (int)(maxWidthHeight * c_syncButtonFloatRect.Y),
                (int)(maxWidthHeight * c_syncButtonFloatRect.Width), (int)(maxWidthHeight * c_syncButtonFloatRect.Height));

            // Resize all the photos based upon the new maxWidthHeight.
            resizedPhotos = ResizeMultipleBitmaps(loadedPhotos, maxWidthHeight);

            // Redraw.
            DrawArtistPhotosPanel();
        }

        public void Global_SelectedArtistChanged(object sender, EventArgs e)
        {
            // Since SelectedArtist could potentially get changed from another thread, we need to assign its value to a local variable.
            // This way we don't create an unstable situation where the value changes mid-process.
            currentArtist = S9PalCommon.Global.SelectedArtist;

            // Enable the track bar and context menus.
            photoSizeTrackBar.Enabled = true;
            getArtistPhotosToolStripMenuItem.Enabled = true;
            browseForPhotosToolStripMenuItem.Enabled = true;

            // Redraw.
            RefreshArtistPhotosControl();
        }

        public void watcher_ArtistPhotoAdded(object sender, ArtistEventArgs e)
        {
            // Refresh the ArtistPhotosControl since an ArtistPhoto has been added.

            // First make sure the control can update. For instance, we don't want to update if the user is currently moving a photo around.
            // This is because all the picture boxes will be cleared off and re-added.
            if (controlCanUpdate)
            {
                if (currentArtist == e.Artist)
                    BeginInvoke(new RefreshArtistPhotosControlDelegate(RefreshArtistPhotosControl));
            }
        }

        #endregion
    }
}
